package edu.hawaii.myisern.model;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import edu.hawaii.myisern.collaborations.jaxb.Collaboration;
import edu.hawaii.myisern.collaborations.jaxb.Collaborations;
import edu.hawaii.myisern.organizations.jaxb.Organization;
import edu.hawaii.myisern.organizations.jaxb.Organizations;
import edu.hawaii.myisern.researchers.jaxb.Researcher;
import edu.hawaii.myisern.researchers.jaxb.Researchers;

/**
 * A Java class presentation of the ISERN social network. This class converts the XML files to the
 * Java classes instances.
 * 
 * @author Edward Kim
 * @author JianFei Liao
 * @author KaYee Leung
 */
public class MyIsernModel {
  /** The singleton MyIsernModel instance. */
  private static MyIsernModel theInstance = new MyIsernModel();

  /** Holds the class-wide JAXBContext, which is thread-safe. */
  private JAXBContext collaborationsJaxbContext;
  /** Holds the class-wide JAXBContext, which is thread-safe. */
  private JAXBContext organizationsJaxbContext;
  /** Holds the class-wide JAXBContext, which is thread-safe. */
  private JAXBContext researchersJaxbContext;

  /** Holds collaborations data. */
  private Collaborations collaborations;
  /** Holds organizations data. */
  private Organizations organizations;
  /** Holds researchers data. */
  private Researchers researchers;
  /** A set for all valid unique IDs */
  private Set<String> uniqueIdSet = new HashSet<String>();
  /** A String represents "user.home". */
  private static final String USER_HOME = "user.home";

  /**
   * Initializes this instance by reading in the specified XML files. There is no error checking
   * since the XML files assume to be correct.
   * 
   */
  private MyIsernModel() {
    // try to load in the researchers XML file
    try {
      this.researchersJaxbContext = JAXBContext.newInstance("edu.hawaii.myisern.researchers.jaxb");
      String directory = System.getProperty(USER_HOME) + "/.myisern/researchers.gold.xml";
      File curFile = new File(directory);
      // copy the sample file if it does not exist
      if (!curFile.exists()) {
        File tempFile = new File(System.getProperty(USER_HOME) + "/.myisern");
        tempFile.mkdir();
        File sourceFile = new File(System.getProperty("user.dir") 
            + "/xml/examples/researchers.example.xml");
        this.copyFile(sourceFile, curFile);
      }
      Unmarshaller unmarshaller = this.researchersJaxbContext.createUnmarshaller();
      System.out.println("Loading researchers data from " + curFile);
      this.researchers = (Researchers) unmarshaller.unmarshal(curFile);
      // add the researchers ID to the unique ID set
      for (Researcher tempResearcher : this.researchers.getResearcher()) {
        this.uniqueIdSet.add(tempResearcher.getName().replace(' ', '_'));
      }
    }
    catch (Exception e) {
      e.printStackTrace();
    }

    // try to load in the Organizations XML file
    try {
      this.organizationsJaxbContext = JAXBContext
          .newInstance("edu.hawaii.myisern.organizations.jaxb");
      String directory = System.getProperty(USER_HOME) + "/.myisern/organizations.gold.xml";
      File curFile = new File(directory);
      // copy the sample file if it does not exist
      if (!curFile.exists()) {
        File tempFile = new File(System.getProperty(USER_HOME) + "/.myisern");
        tempFile.mkdir();
        File sourceFile = new File(System.getProperty("user.dir") 
            + "/xml/examples/organizations.example.xml");
        this.copyFile(sourceFile, curFile);
      }
      Unmarshaller unmarshaller = this.organizationsJaxbContext.createUnmarshaller();
      System.out.println("Loading organizations data from " + curFile);
      this.organizations = (Organizations) unmarshaller.unmarshal(curFile);
      // add the organizations ID to the unique ID set
      for (Organization tempOrganization : this.organizations.getOrganization()) {
        this.uniqueIdSet.add(tempOrganization.getName().replace(' ', '_'));
      }
    }
    catch (Exception e) {
      e.printStackTrace();
    }

    // try to load in the Collaborations XML file
    try {
      this.collaborationsJaxbContext = JAXBContext
          .newInstance("edu.hawaii.myisern.collaborations.jaxb");
      String directory = System.getProperty(USER_HOME) + "/.myisern/collaborations.gold.xml";
      File curFile = new File(directory);
      // copy the sample file if it does not exist
      if (!curFile.exists()) {
        File tempFile = new File(System.getProperty(USER_HOME) + "/.myisern");
        tempFile.mkdir();
        File sourceFile = new File(System.getProperty("user.dir") 
            + "/xml/examples/collaborations.example.xml");
        this.copyFile(sourceFile, curFile);
      }
      Unmarshaller unmarshaller = this.collaborationsJaxbContext.createUnmarshaller();
      System.out.println("Loading collaborations data from " + curFile);
      this.collaborations = (Collaborations) unmarshaller.unmarshal(curFile);
      // add the collaborations ID to the unique ID set
      for (Collaboration tempCollab : this.collaborations.getCollaboration()) {
        this.uniqueIdSet.add(tempCollab.getName().replace(' ', '_'));
      }
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * Gets the single instance of MyIsernModel object.
   * 
   * @return A MyIsernModel.
   */
  public static synchronized MyIsernModel getInstance() {
    return MyIsernModel.theInstance;
  }

  /**
   * Gets all the researchers as a map
   * 
   * @return researchers map
   */
  public synchronized Map<String, Researcher> getResearchersMap() {
    Map<String, Researcher> map = new HashMap<String, Researcher>();
    for (Researcher tempResearcher : this.researchers.getResearcher()) {
      map.put(tempResearcher.getName().replace(' ', '_'), tempResearcher);
    }
    return map;
  }

  /**
   * Gets all the organizations as a map
   * 
   * @return organizations map
   */
  public synchronized Map<String, Organization> getOrganizationsMap() {
    Map<String, Organization> map = new HashMap<String, Organization>();
    for (Organization tempOrg : this.organizations.getOrganization()) {
      map.put(tempOrg.getName().replace(' ', '_'), tempOrg);
    }
    return map;
  }

  /**
   * Get all the collaborations as a map
   * 
   * @return collaborations map
   */
  public synchronized Map<String, Collaboration> getCollaborationsMap() {
    Map<String, Collaboration> map = new HashMap<String, Collaboration>();
    for (Collaboration tempCollab : this.collaborations.getCollaboration()) {
      map.put(tempCollab.getName().replace(' ', '_'), tempCollab);
    }
    return map;
  }

  /**
   * Gets a single researcher
   * 
   * @param id The unique id of the researcher
   * @return a single researcher
   */
  public synchronized Researcher getSingleResearcher(String id) {
    return this.getResearchersMap().get(id);
  }

  /**
   * Gets a single organization
   * 
   * @param id The unique ID for an organization
   * @return a single organization
   */
  public synchronized Organization getSingleOrganization(String id) {
    return this.getOrganizationsMap().get(id);
  }

  /**
   * Gets a single collaboration
   * 
   * @param id The unique ID for a collaboration
   * @return a single collaboration
   */
  public synchronized Collaboration getSingleCollaboration(String id) {
    return this.getCollaborationsMap().get(id);
  }

  /**
   * Gets all the researchers as a list
   * 
   * @return A list containing all the researchers.
   */
  public synchronized List<Researcher> getResearchersList() {
    List<Researcher> list = new ArrayList<Researcher>();
    for (Researcher tempResearcher : this.researchers.getResearcher()) {
      list.add(tempResearcher);
    }
    return list;
  }

  /**
   * Gets all the organizations as a list
   * 
   * @return A list containing all the organizations.
   */
  public synchronized List<Organization> getOrganizationsList() {
    List<Organization> list = new ArrayList<Organization>();
    for (Organization tempOrganization : this.organizations.getOrganization()) {
      list.add(tempOrganization);
    }
    return list;
  }

  /**
   * Gets all the collaborations as a list
   * 
   * @return A list containing all the collaborations.
   */
  public synchronized List<Collaboration> getCollaborationsList() {
    List<Collaboration> list = new ArrayList<Collaboration>();
    for (Collaboration tempCollaboration : this.collaborations.getCollaboration()) {
        list.add(tempCollaboration);
    }
    return list;
  }
  
  /**
   * Adds or edits a researcher.
   * 
   * @param researcher The researcher to be added or edited.
   * @return The Researchers object after the adding or editing.
   */
  public synchronized Researchers addOrEditResearcher(Researcher researcher) {
    // get the unique ID
    String id = researcher.getName().replace(' ', '_');
    // if the researcher already exists
    if (this.getResearchersMap().containsKey(id)) {
      Researcher tempResearcher = this.getResearchersMap().get(id);
      // construct a new Researchers object without the old researcher
      int index = this.getResearchersList().indexOf(tempResearcher);
      List<Researcher> list = this.getResearchersList();
      list.set(index, researcher);
      Researchers researchers = new Researchers();
      researchers.getResearcher().addAll(list);
      this.researchers = researchers;
      return this.researchers;
    }
    // if the researcher is new
    else {
      this.researchers.getResearcher().add(researcher);
      this.uniqueIdSet.add(id);
      return this.researchers;
    }
  }

  /**
   * Adds or edits an organization.
   * 
   * @param organization The organization to be added or edited.
   * @return The Organizations object after the adding or editing.
   */
  public synchronized Organizations addOrEditOrganization(Organization organization) {
    // get the unique ID
    String id = organization.getName().replace(' ', '_');
    // if the organization already exists
    if (this.getOrganizationsMap().containsKey(id)) {
      Organization tempOrganization = this.getOrganizationsMap().get(id);
      // construct a new Organizations object without the old organization
      int index = this.getOrganizationsList().indexOf(tempOrganization);
      List<Organization> list = this.getOrganizationsList();
      list.set(index, organization);
      Organizations organizations = new Organizations();
      organizations.getOrganization().addAll(list);
      this.organizations = organizations;
      return this.organizations;
    }
    // if the organization is new
    else {
      this.organizations.getOrganization().add(organization);
      this.uniqueIdSet.add(id);
      return this.organizations;
    }
  }

  /**
   * Adds or edits a researcher.
   * 
   * @param collaboration The collaboration to be added or edited.
   * @return The Collaborations object.
   */
  public synchronized Collaborations addOrEditCollaboration(Collaboration collaboration) {
    // get the unique ID
    String id = collaboration.getName().replace(' ', '_');
    // if the collaboration already exists
    if (this.getCollaborationsMap().containsKey(id)) {
      Collaboration tempCollaboration = this.getCollaborationsMap().get(id);
      // construct a new Collaborations object without the old collaboration
      int index = this.getCollaborationsList().indexOf(tempCollaboration);
      List<Collaboration> list = this.getCollaborationsList();
      list.set(index, collaboration);
      Collaborations collaborations = new Collaborations();
      collaborations.getCollaboration().addAll(list);
      this.collaborations = collaborations;
      return this.collaborations;
    }
    // if the collaboration is new
    else {
      this.collaborations.getCollaboration().add(collaboration);
      this.uniqueIdSet.add(id);
      return this.collaborations;
    }
  }
  
  /**
   * Saves the data changes by writing to the XML files.
   */
  public synchronized void save() {
    // save all changes for researchers
    try {
      JAXBContext researchersJaxbContext = JAXBContext
          .newInstance("edu.hawaii.myisern.researchers.jaxb");
      Marshaller marshaller = researchersJaxbContext.createMarshaller();
      // set the marshaller to do formatted output to the XML file
      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
      // writes the new XML
      OutputStream os = new FileOutputStream(System.getProperty(USER_HOME) + 
          "/.myisern/researchers.gold.xml");
      marshaller.marshal(this.researchers, os);
    }
    catch (Exception e) {
      e.getMessage();
      e.printStackTrace();
    }
    try {
      // save all changes for organizations
      JAXBContext organizationsJaxbContext = JAXBContext
          .newInstance("edu.hawaii.myisern.organizations.jaxb");
      Marshaller marshaller = organizationsJaxbContext.createMarshaller();
      // set the marshaller to do formatted output to the XML file
      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
      // writes the new XML
      OutputStream os = new FileOutputStream(System.getProperty(USER_HOME) + 
          "/.myisern/organizations.gold.xml");
      marshaller.marshal(this.organizations, os);
    }
    catch (Exception e) {
      e.getMessage();
      e.printStackTrace();
    }
    try {
      // save all changes for collaborations
      JAXBContext collaborationsJaxbContext = JAXBContext
          .newInstance("edu.hawaii.myisern.collaborations.jaxb");
      Marshaller marshaller = collaborationsJaxbContext.createMarshaller();
      // set the marshaller to do formatted output to the XML file
      marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
      // writes the new XML
      OutputStream os = new FileOutputStream(System.getProperty(USER_HOME) + 
          "/.myisern/collaborations.gold.xml");
      marshaller.marshal(this.collaborations, os);
    }
    catch (Exception e) {
      e.getMessage();
      e.printStackTrace();
    }
  }
  
  /**
   * Helper method to copy the sample XML files.
   * This method is inspired from http://www.rgagnon.com/javadetails/java-0064.html
   * 
   * @param in Source file.
   * @param out Destination file.
   * @throws Exception If error occurs.
   */
  private synchronized void copyFile(File in, File out) throws Exception {
    FileChannel inChannel = new
    FileInputStream(in).getChannel();
    FileChannel outChannel = new FileOutputStream(out).getChannel();
    try {
      inChannel.transferTo(0, inChannel.size(), outChannel);
      System.out.println("Copying from " + in + " to " + out);
    } 
    catch (Exception e) {
      e.getMessage();
      e.printStackTrace();
    }
    finally {
      if (inChannel != null) { 
        inChannel.close();
      }
      if (outChannel != null) { 
        outChannel.close();
      }
    }
  }
}